// Copyright (c) 2017 Uber Technologies, Inc.
//

import viz from 'viz.js/viz.js'

import convPlain from './dot/convPlain'
import toDot from './dot/toDot'

import { EWorkerPhase, TLayoutOptions } from './types'
import { TEdge, TLayoutVertex, TSizeVertex } from '../types'

enum EValidity {
  Ok = 'Ok',
  Warn = 'Warn',
  Error = 'Error',
}

type TValidityError = {
  validity: EValidity.Error
  message: string
}

type TValidityOk = {
  validity: EValidity.Ok
  message: null
}

type TValidityWarn = {
  validity: EValidity.Warn
  message: string
}

type TVerticesValidity = TValidityError | TValidityOk | TValidityWarn

const SHIFT_THRESHOLD = 0.015

function isCloseEnough(a: number, b: number) {
  return Math.abs(a - b) / b < SHIFT_THRESHOLD
}

function getVerticesValidity(
  input: TSizeVertex[] | TLayoutVertex[],
  output: TLayoutVertex[],
): TVerticesValidity {
  const inputHash: { [key: string]: TSizeVertex | TLayoutVertex } = {}
  input.forEach((v) => {
    inputHash[String(v.vertex.key)] = v
  })

  let warn: TVerticesValidity | void

  for (let i = 0; i < output.length; i++) {
    const {
      vertex: { key },
      height,
      left,
      top,
      width,
    } = output[i]
    const src = inputHash[String(key)]
    if (!src) {
      return {
        validity: EValidity.Error,
        message: `Extra vertex found: ${key}`,
      }
    }
    if (
      !isCloseEnough(src.height, height) ||
      !isCloseEnough(src.width, width)
    ) {
      return {
        validity: EValidity.Error,
        message: `Vertex ${key} failed size threshhold check (${SHIFT_THRESHOLD})`,
      }
    }
    if ('left' in src && 'top' in src) {
      const { left: srcLeft, top: srcTop } = src
      if (!isCloseEnough(srcLeft, left) || !isCloseEnough(srcTop, top)) {
        warn = {
          validity: EValidity.Warn,
          message: `Vertex ${key} failed position threshhold check (${SHIFT_THRESHOLD})`,
        }
      }
    }
    delete inputHash[String(key)]
  }
  const missingKeys = Object.keys(inputHash)
  if (missingKeys.length !== 0) {
    const word = missingKeys.length > 1 ? 'vertices' : 'vertex'
    return {
      validity: EValidity.Error,
      message: `Missing ${word}: ${missingKeys.join(', ')}`,
    }
  }
  warn ??= { validity: EValidity.Ok, message: null }
  return warn
}

export default function getLayout(
  phase: EWorkerPhase,
  inEdges: TEdge[],
  inVertices: TSizeVertex[] | TLayoutVertex[],
  layoutOptions: TLayoutOptions | null,
) {
  const dot = toDot(inEdges, inVertices, layoutOptions)
  const { totalMemory = undefined } = layoutOptions || {}
  const options = {
    totalMemory,
    engine: phase === EWorkerPhase.Edges ? 'neato' : 'dot',
    format: 'plain',
  }
  const plainOut = viz(dot, options)
  const { edges, graph, vertices } = convPlain(
    plainOut,
    phase !== EWorkerPhase.Positions,
  )
  const result = getVerticesValidity(inVertices, vertices)
  if (result.validity === EValidity.Error) {
    const message = result.message
    return {
      graph,
      edges,
      vertices,
      layoutError: true,
      layoutErrorMessage: message,
    }
  }
  if (result.validity === EValidity.Warn) {
    return {
      graph,
      edges,
      vertices,
      layoutErrorMessage: result.message,
    }
  }
  return { edges, graph, vertices }
}
